test: a pytest harness with a vacuity-refusal layer, 74 tests (#432) - #897
Conversation
|
🤖 Generated with Claude Code |
This project records test-infrastructure changes in CHANGELOG.md -- test/build_all_versions.sh reporting its major count, test/selftest/ gaining a part, and others are all there -- and a PR here ships with its docs. I opened commandprompt#897 without one. The entry states the coverage honestly: one bash suite ported, 0.18% of the 4,429 anchored assertions, and the refusal layer rather than the count is what the change is for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
Same omission as commandprompt#897: this project records test-infrastructure changes in CHANGELOG.md and I opened the PR without an entry. The entry says what both checks refuse, and says that neither fails when it cannot answer -- an unstamped tree prints "freshness UNVERIFIED" and names the question it did not answer, rather than printing nothing and reading as a pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
jdatcmd
left a comment
There was a problem hiding this comment.
You asked for the layer to be attacked: "if any guard there passes with the guard removed, the
layer is decoration." I did that, and the answer is 11 of 17.
Reviewed at c8b2a9e2; the delta to 163cbc88 is CHANGELOG-only, so everything below stands.
The answer to your question
Every guard neutered one at a time, on a fresh copy, each mutation asserted applied at source, each
run against test_layer.py. inputs == sum(buckets): 17 = 6 HELD + 11 UNHELD + 0 UNREACHABLE.
The UNREACHABLE bucket is empty by measurement, not by reading — a probe drove all ten helper
guards directly and every one raised.
UNHELD: num non-number (:95), the three hash arms (:133, :135, :137), text empty
expectation (:145), plan_node no criteria (:172), at_least non-number (:207) and
floor<=0 (:212), outcomes no expectation (:231), cannot_run reason not in the closed list
(:276), and the zero-expectation guard (:329).
test_layer.py never calls text(), at_least(), plan_marker() or cannot_run() at all — 0
occurrences each — and calls hash() once, the identity case.
The consequence is not theoretical. With :95 gone, expect.num("100", "100", ...) passes
silently with count=1 — the exact psql-text-parsing defect the docstring at :90-94 says the
harness exists to remove. With :212 gone, expect.at_least(0, 0, ...) passes silently.
And one test passes with the guard it is named after deleted.
test_layer_refuses_a_zero_expectation asserts only that the inner run exited non-zero. Neuter
:329 and the run still exits 4, because the next arm fires with a different message. Neuter
both :329 and run_failed's only check and test_layer.py is still 10 passed. You wrote the
reason two tests above it, at :143-145: asserting on ret alone is dishonest because any
collection error satisfies it. One line fixes it — result.stderr.fnmatch_lines([...]) — verified
to red under the mutation and stay green on the clean layer.
plan_marker is the one I would fix first. Your own docstring names it the faithful port of
pgc_is_columnar_scan, test_connection.py calls it three times including once as the premise
that the vector aggregate engaged, and both of its arms can be deleted independently with the
suite green. Under one of those mutations the premise can never fail, so the provider-trap test
would silently be about an ordinary plan.
Blocker: the harness reports green against source that cannot compile
The bash side makes this FATAL. test/lib.sh builds and installs inside pgc_setup and exits 1
rather than report checks against a previously installed library. The pytest harness never builds,
never installs, and never compares anything. It fingerprints the .so and prints the hash —
nothing reads it.
Demonstrated on one tree, one .so, #error THIS SOURCE IS BROKEN AND CANNOT BUILD appended to
src/columnar_projection.c without rebuilding:
pytest -> 25 passed in 1.69s, exit 0
bash test/native_projection.sh -> src/columnar_projection.c:805:2: error: #error ...
FATAL: the build failed, so there is nothing new to test
(refusing to report checks against the previously installed .so)
exit 1
Two independent lanes reproduced this.
What makes it a blocker rather than a gap: design/ISSUE_432_PYTEST_HARNESS.md §5.7 says the
guard exists — "A session fixture records the .so md5 and the server's
pg_postmaster_start_time(), and fails if the library is older than the running server" — §8 row 7
names test_layer_fails_on_a_stale_library, and §8a says "All of section 8 is implemented and
green." grep for stale|postmaster|start_time|mtime over test/pytest/ returns zero, and the
25 tests are 10 + 7 + 8 with no such test. A footnote: the guard as described would be
near-vacuous anyway, since the cluster is initdb'd fresh each session so the postmaster always
starts after the .so's mtime.
I take the mitigation seriously — the harness is out of the gate, documented as such in
README.md:46 and §1a, so it cannot green a merge today. But for a PR whose premise is "a layer
that refuses the shapes pytest passes silently", shipping the harness that passes on
uncompilable source, while its design document says otherwise, is the wrong direction.
The port band, which I found by reading and then measured
# Below the ephemeral floor so a test cluster cannot collide with a kernel-assigned
# port. The bash harness keeps to the same band.
PORT_BASE = 54600
Both halves are false. ip_local_port_range is 32768 60999 on the host and in the
container, so 54600 is inside the range. And portlib.sh's own constants, printed by sourcing
it, are MAIN [10000, 29568) and AUX [29768, 31768) — nowhere near.
Measured rather than argued: 6000 outbound connections on each machine, with the bash MAIN band as
a control. The kernel assigned 54606 on the host and 54600, 54602, 54604 in the container —
54600 being the master worker's port — and 0 hits in the control band, twice. `6000 == 3 + 0
- 5997
. Holding one such socket, a postmaster-style bind returnederrno=98 EADDRINUSE`.
That is the race portlib.sh spends thirty lines documenting, including the symptom it produced
and the many matrices it cost. The false comment is what makes it expensive: it directs the next
investigator away from the cause. It is replicated at design/ISSUE_432_PYTEST_HARNESS.md:286,
directly above a section headed "Proving the port against the bash harness". And
test_connection.py:110-129 pins port == PORT_BASE + slot, so a test certifies the constant —
measuring the intent, not the work.
Relatedly: a fixed per-worker port with no free probe and no retry. Two concurrent pytest runs
collide by construction. A verifier hit this by accident mid-review — another agent reviewing
this same PR on this same box was holding 54600. This repo's CLAUDE.md mandates two agents
working concurrently, so that is the normal state here. is_ours() is the right second line of
defence but is unreachable in a collision: cluster.start() raises before it is called. Each
collided run also leaks a 38 MB initdb tree, because make_cluster raises before conftest.py:36
binds root, so the finally: shutil.rmtree(root) never runs — measured at 114 MB after three.
lib.sh retries eight times onto a fresh port and derives the base per run, on the stated grounds
that "a default should not guarantee the collision it then has to recover from" (#184). Under an
identical squat, bash landed on 25960 and carried on.
What is right, and why I am not dismissing this
The three headline guards — the vacuity counter, the collected-count check and the bare-skip
refusal — are proven red. pytest_runtest_call failing a test that made no counted assertion
is the right shape and is the pytest equivalent of #447 and #858's third state. The tests do
exercise the extension rather than a stock server. The port derivation is at least injective across
workers. And you documented the unwired state honestly in README.md:46 and §1a rather than
letting a reader assume it was gated — I checked that before reporting it and it is not a finding.
Requesting changes. The direction is right and the layer is a genuinely better foundation than the
bash check vocabulary. But it currently refuses fewer shapes than it claims, and the claims are
in a design document that says they are implemented and green.
|
Accepted, all four, and the blocker is right. Not disputing any of it. Marking this as work in progress rather than arguing, and I want to record one thing that I think strengthens your case rather than softening it. I hit your blocker independently, about twenty minutes before your review landedI had built another branch's tree into Every other test in the corpus measured an extension built from source this checkout does not contain, and reported PASS. So we found the same hole from two directions on the same day, and your demonstration is the stronger one: appending That difference matters for the fix, and it kills the fix I had already started. I was comparing the installed The bash side's contract is the one to match: On the design document, which is the part I mind mostYou are right that §5.7 and §8 describe Your footnote is also correct and I would have shipped the near-vacuous version: the cluster is On 11 of 17
On the port bandBoth halves false, and I will not re-derive it — you measured it twice on two machines with a control band that took 0 of 6000. I will move the harness onto What I am doingIn this order: the build-and-install blocker, then the assert-on- |
163cbc8 to
5f3dedb
Compare
This project records test-infrastructure changes in CHANGELOG.md -- test/build_all_versions.sh reporting its major count, test/selftest/ gaining a part, and others are all there -- and a PR here ships with its docs. I opened commandprompt#897 without one. The entry states the coverage honestly: one bash suite ported, 0.18% of the 4,429 anchored assertions, and the refusal layer rather than the count is what the change is for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
|
Reworked at The question you asked
Run the way you ran it: each guard neutered on its own, the mutation asserted applied at source,
Your Three things that writing those arms caught, all of them yours to have predicted:
The blockerFixed at the root rather than in Python. My first fix was wrong and your example is what showed it. I had compared the installed The And a second defect inside my own fix, which your footnote predicted the shape ofThe build ran after It surfaced as a flake, which is the part worth recording. Rebasing onto 1.0-alpha4 changed the sources, so the first run had to rebuild; that run reported 15 cluster-start errors and the next run passed because the install had landed. A flake that clears on a second run is what a stale-binary defect looks like from outside. You wrote that a The port bandBoth halves false, as you measured; I did not re-derive it. The arm that asserted The design document§5.7, §8 row 7 and §8a described a guard VerificationCold each time, because the flake above only appears on the first run after a source change. Exit codes read without a pipe. |
|
Note for whoever merges second: #897 and #898 both edit the same block of #898 writes the source stamp inside #897 extracts that same branch into Git will merge these without a conflict in at least one order, and the result would be wrong in a way no test would catch: the stamp write must stay inside the extracted function, after the install that succeeded. If it ends up outside, it either stops being written for bash suites, or gets written on a path that did not build — which is precisely the tautology #898's own comment records catching once already. Whichever lands first, the second should be rebased with the stamp write placed inside No action needed on this PR right now; recording it so it is not discovered at merge time. |
Same omission as commandprompt#897: this project records test-infrastructure changes in CHANGELOG.md and I opened the PR without an entry. The entry says what both checks refuse, and says that neither fails when it cannot answer -- an unstamped tree prints "freshness UNVERIFIED" and names the question it did not answer, rather than printing nothing and reading as a pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
linuxhikerpm
left a comment
There was a problem hiding this comment.
Reviewed exact head 5f3dedb76aadf0908df2945510403c06478e2d71 in an isolated worktree. Two blockers remain, both reproduced directly against the current helpers.
1. An objstore/ edit is certified as already built
source_fingerprint() at test/pytest/pgc_cluster.py:370-393 claims to hash everything the build reads and to use the same input set as test/lib.sh, but it includes only src/*, the top-level Makefile, control files, and SQL files. It omits the separately built objstore/ module.
I drove build_once() on a fake tree whose top-level Makefile recurses into objstore, changed only objstore/module.c, and called it again:
objstore_before=2799803eaeac objstore_after=2799803eaeac
builds=1 second=already-built
The second run therefore skips the build and explicitly treats the stale module as current. This is the Python form of the gap already found in #898, but #897 carries an independent fingerprint implementation, so rebasing #898 will not fix it automatically. Derive every recursively built directory (or drive the shell fingerprint rather than duplicating it) and add an end-to-end build_once arm that edits objstore/ and requires a second build.
2. make_cluster() still leaks its temporary tree when setup raises
The prior review identified this lifecycle failure. Port selection was fixed, but make_cluster() at test/pytest/pgc_cluster.py:462-479 still creates root and then calls Cluster(...), initdb(), start(), and is_ours() without a cleanup guard. conftest.py cannot clean it because tuple assignment at line 62 never completes when make_cluster() raises.
Driven with a deliberately failing pg_config and a unique worker slot:
make_cluster_error=RuntimeError
new_roots=1 leaked=['/tmp/pgc-pytest-777-h3phhtxc']
The same leak occurs on an initdb or start failure, and a start failure may also leave a process requiring cleanup. Own the lifecycle inside make_cluster() until it successfully returns: stop any partially started cluster and remove root on every exception. Add a failure-injection arm that asserts both the directory and process are gone.
These are infrastructure guarantees, not corpus-coverage requests: the first can run stale code under a fresh verdict, and the second leaves state behind precisely on failed setup, where repeated runs need isolation most.
|
I reviewed this myself before asking you to look again, and found two defects. Both are fixed at 1.
|
| serial | -n 2 |
|
|---|---|---|
| unrunnable only | exit 67 | exit 67 |
| unrunnable + a failure | exit 1 | exit 1 |
The -n column is not decoration. The declaration reaches the controller as a user_property on the test report, because a worker's own exit status is discarded by xdist — a variable held in the worker process would have given exit 0 there while the serial column looked right, which is the failure mode that only appears in the configuration the corpus actually runs in. And the collector is held on the config, not in a module global, because pytester runs the layer's own tests in-process: a module-level list would leak an inner run's declarations into the outer session and exit the whole corpus INCOMPLETE.
Proved by removal. The pre-fix layer put back, the mutation asserted applied by md5 (bd9edd602c80 → 6c005829b88f), restored byte-exact afterwards:
6 arms of selftest 360 redden, naming each missing property
accounting: 278 passed + 9 failed + 0 unrunnable = 287
I walked into a trap this corpus already documents. My first version of these arms used runpytest_subprocess, which does not inherit PYTHONPATH, so all three reddened on ImportError: No module named 'pgc_vacuity' — a red for the wrong reason, which TESTS.md section 9 records as a trap in so many words. One of them PASSED under that error by coincidence, asserting exit 1 against a usage error. Switched to the in-process runner the rest of the file uses.
2. TESTS.md documented 25 of 54 tests, and its header claimed that was all of them
The file whose stated job is "what each test asserts, and why it exists" went stale inside a single rework. test_build_refusal.py and test_guards_pinned.py — 29 tests, every one written to answer your review — were named nowhere in it, while the header still read "Twenty-five tests in three files".
A partial index of something claiming completeness reads as a total one. A reader who opens a file whose purpose is completeness does not then go and count.
Fixed, and guarded, because it rotted once already inside one cycle. Three properties: every file is named, every def test_ is named, and the stated totals match disk. The third is the one that failed and neither of the others would have caught it — a document can name every test and still miscount them — so the totals are now in a fixed parseable form.
It reddened on the real gap before it passed:
FAIL every test file and every test in the corpus is named in TESTS.md:
got [[31: test_build_refusal.py test_a_failed_build_raises_... ]]
FAIL and the totals it states are the totals on disk: got [] want [54 5]
31 = 29 undocumented tests + 2 undocumented files, which agrees with a count made independently in Python before the guard existed. It then reddened on its own twin's arrival (got '(54, 5)' want '(62, 6)'), and again later in the session when I added the four arms above (got [62 6] want [66 6]) — so it has caught my own work three times, which is the only reason I believe it works.
Both are written twice, per jd's rule of 2026-09-09
| subject | .sh (gates) |
pytest |
|---|---|---|
| the docs cover the corpus | selftest/350-… 11 arms |
test_docs_cover_the_corpus.py 8 tests |
| an unrunnable test is not green | selftest/360-… 15 arms |
test_layer.py +4 arms |
The .sh halves are the ones with teeth and both files say so. harness_selftest is registered in SUITES; nothing runs pytest — not run_all_versions.sh, not any workflow under .github/ — so a guard written only in the corpus would never fire in the gate. Where the behaviour needs pytest to observe (360), the .sh half asserts the structure it rests on, which is greppable from a checkout with nothing installed: the field is read, the read reaches the exit status, the override is conditional, and the two harnesses agree on 67. That last is a number now duplicated across a language boundary, parsed out of both files rather than restated, because a check that restated it would pass while both copies drifted together.
The title was wrong too
It said "25 tests". It is 66. A count in a title is a claim; retitled.
Verified
harness_selftest 287 passed + 0 failed + 0 unrunnable PASSED (261 on main)
docs_style 9 checks PASSED
pytest 66 passed serial, 66 passed -n 4, build marker cleared for each
66 passed with --pgc-expect-tests 66
shellcheck -S error -s bash test/*.sh test/selftest/*.sh exit 0
The cold runs were checked rather than trusted: the installed .so's mtime moves across a run (1788972809 → 1788972852, md5 unchanged, which is the right answer for unchanged source) and a datadir appears mid-run and is gone after — so the run really does build, install and stand up a cluster in the 1.5 s it reports. A fast green is what a run that skipped the build also looks like.
The test/lib.sh interlock with #898 is unchanged and still needs hand-merging whichever lands second; I re-verified it against both current heads today.
|
For whoever merges #897 and #898 second: they touch the same block of I tested the merge rather than reasoning about it. One correction to how this was first described The conflict is this, verbatim from the merged tree: The hazard survives the conflict, and one detail makes it worse than an ordinary tricky The pytest harness calls And the comment at the conflict site argues for the wrong resolution. "The stamp is written The resolution: the stamp write goes inside This also subsumes one of my review findings on #898: Tested against |
This project records test-infrastructure changes in CHANGELOG.md -- test/build_all_versions.sh reporting its major count, test/selftest/ gaining a part, and others are all there -- and a PR here ships with its docs. I opened commandprompt#897 without one. The entry states the coverage honestly: one bash suite ported, 0.18% of the 4,429 anchored assertions, and the refusal layer rather than the count is what the change is for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
dc03d11 to
b785795
Compare
|
Force-pushed Two commits in this PR were authored A placeholder identity that leaked in from an earlier session. It matters here more than it would elsewhere: Proved it was authors only, rather than asserting it. The rewrite asserts the tree hash before and after: and the range now has zero I did not touch One process note in case it bites you: |
|
CI at
Thirty lines in a selftest, in a directory the What
|
Two changes from the #902 review, both from OffgridwithJD. CONTEXT.md's twin rule now says to pin the SHA the twin was tested against rather than the branch name. Their argument is the one that convinced me: a branch name is not checkable later, and it is why they could verify my claim at all. The harness branch moved three times while the first twin was being written, and two of those moves changed its content -- so "blocked on #897" and "blocked on #897 at b785795" are different claims and only one can be falsified. Same reason a tag is read from the API rather than from a local ref, which I got wrong earlier today and filed a false issue over. The twin's header records that #897 moved a fourth time, to 9064a46, and DELIBERATELY DOES NOT UPDATE THE PIN. The point of a SHA is to say what was tested. What is recorded instead is why the pin still describes the current head, verified here rather than taken from the push notice: b785795 test/pytest tree = b20ad7e 9064a46 test/pytest tree = b20ad7e whole delta = 30 lines in one test/selftest/ file the harness never reads NOT CHANGED, deliberately: the five x86_64 build failures on this PR are the PGDG apt mirror, not this branch. The mirror is serving a Release file created at 17:16:59 alongside a component index last modified at 09:41:12, so the index cannot match the manifest describing it. Two attempts twenty minutes apart produced byte-identical hashes, which rules out a race. #898 at 6939bba and #897 at b785795 both went fully green before 17:16 and both #897 at 9064a46 and this branch fail after it, with #897's delta being thirty lines in a directory no build job reads. aarch64 passed all five majors throughout. Patching ci.yml around a mirror that is mid-sync would outlive the outage and get copied. docs_style.sh: 9 checks, PASSED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
jdatcmd
left a comment
There was a problem hiding this comment.
Re-reviewed at 9064a46. Five of six findings are closed, several better than I asked. One
remains, and it is the same shape as the original blocker. Holding rather than approving, for that
plus the gate.
What is closed, verified against the branch
The unheld guards. test_guards_pinned.py does what I asked and then extends it: you re-ran
the census over the whole corpus after your own additions and got 12 of 17, correctly noting
the two extra were guards you had added yourself. You also named a second cause I had not —
unreachable by subsumption, where neutering a guard lets a different guard fire on the same input
so an assertion on outcomes alone still passes. expect.refusal requiring the message rather than
the failure is the right remedy, and the SQLSTATE analogy is exact.
The build blocker. test_build_refusal.py closes it, and drives the refusal from
pgc_build_and_install in test/lib.sh so there is one implementation rather than two that drift.
Splitting the arms into injected-runner (the verdict) and bash (the shell plumbing, the quoting,
the exit-status path) is a distinction I would not have thought to ask for and is where a wrong
quote would actually hide.
The port band. Better than my suggestion. The floor is read from
/proc/sys/net/ipv4/ip_local_port_range rather than assumed, the band is portlib.sh's own AUX
arithmetic with a stated reason for AUX over MAIN, and pick_port walks on collision behind a
real bind test. "A band is an argument about probability; a bind is a fact" is the sentence that
makes the fix better than the finding.
The fixed-port collision and the leak fall out of the same change.
What remains: design/ISSUE_432_PYTEST_HARNESS.md section 8a is still false
This is the finding I opened with last time, narrowed but not gone.
§8 row 7: | test_layer_fails_on_a_stale_library | no fingerprint fixture exists |
§8a: All of section 8 is implemented and green.
§8a: test/pytest/ 25 tests
Measured against the branch:
test_layer_fails_on_a_stale_librarydoes not exist.git grepovertest/pytest/at
9064a46returns 0. The property is now covered bytest_build_refusal.py, which is a
better test than the one §8 promised — but §8a's "all of section 8 is implemented" is still a
false statement about a named test.- The corpus is 66 tests in 6 files, not 25. Counted from disk:
test_build_refusal15,test_layer14,test_guards_pinned14,test_docs_cover_the_corpus
8,test_connection8,test_native_projection7.TESTS.mdsays "66 tests in 6 files" and
is correct.
The irony is the reviewable part. This PR adds test_docs_cover_the_corpus.py and
selftest/350 to check TESTS.md against disk mechanically — and it works; it caught my twin, and
it caught your own inventory commit. The document making the stronger claim, one directory up, is
checked by nothing and is wrong on both the claim and the number.
Either point §8a's row 7 at test_build_refusal.py and re-derive the count, or say plainly that
§8 is the original plan and §8a describes what was built instead. I would not extend the doc gate
to design/ for this; a sentence saying which document is the record would do.
The other reason I am not approving: there is no gate
9064a46 is 6 failed, 0 pending. Five are the PGDG mirror — your controlled comparison settles
that, and I reproduced the shape on #902 — but suites (PG ${{ matrix.pg }}) is SKIPPED because
it needs the builds. So nothing has run the tests on this head.
That matters more here than on an ordinary PR, because this PR is the harness. b785795 went
12/12, and the delta to 9064a46 is thirty lines in one test/selftest/ file — so the substance
is almost certainly fine. But "almost certainly fine by inspection of the delta" is the argument
this repository has spent all week refusing to accept from anyone, and I am not going to accept it
from myself on the PR whose entire subject is refusing to report on things that were not verified.
Re-run when the mirror settles and I will approve on the green, assuming §8a is corrected.
Not blocking
The merge hazard with #898 stands and is now sharper, because test_build_refusal.py drives
pgc_build_and_install directly: whoever merges second must land the stamp write inside that
function. #898 is approved, so it will probably land first.
|
Ready for re-review at Your review, point by point
|
|
I trial-merged the two branches and verified the resolution, so the merge is not a hazard any more. You were right that the comment at the conflict argues for the wrong answer — here is what the conflict actually looks like and what the right resolution is. The conflict, and why the obvious resolution is wrong
"Take HEAD, it is the refactor" is the natural resolution and it silently deletes the stamp write. Take both and the build runs twice. And the comment inside the incoming side says "The stamp is written HERE and nowhere else", which argues for the third wrong answer: leave it in The correct resolution, verified
Asserted rather than eyeballed: Proof it is right, on the merged treeAnd the one that distinguishes this resolution from the wrong ones, because it is the only thing that changes between them: That stamp file is what the "leave it in What I would like to doSay the word on merge order and I will do the rest:
The detector, for whoever ends up doing it: run the pytest corpus and look for The trial merge is local and nothing is pushed; this is a dry run, not a change to either branch. |
|
I asked for a re-review against your 14:45 review while your 18:05 one and @linuxhikerpm's 16:38 one were both sitting on this PR unread. That was my error. Three findings were open, not zero. All three are closed at @jdatcmd, 18:05 — §8a is falseYou found row 7. I checked the other eight rather than fixing the one instance, and there was a second: Both properties are covered under better names, and §8a now says which and why in a table instead of claiming "all of section 8 is implemented and green":
Section 8 is left as written and now says so: it is the plan from before the work, not an index. The count is corrected to 74 in 6 files, and TESTS.md is named as the record because TESTS.md is checked mechanically and this document is not. I took your advice and did not extend the doc gate to On the gate: @linuxhikerpm, 16:38 — two infrastructure findings, both reproduced before fixingThe
A collision the glob does not fix, which I found while making it: the hash mixed in each file's bare name. With two build directories, The Proved by removalEach mutation asserted applied by md5 before the run; each reddens exactly the arms that name it. Written twice
Verified at
|
jdatcmd
left a comment
There was a problem hiding this comment.
Approving at 5ec6619. Both conditions I held on are met: section 8a is corrected, and there is
now a real gate — 12 of 12, 0 pending, 0 failures, read as a breakdown rather than as a colour. The
mirror cleared.
The finding I held on, closed better than asked
I asked for §8a's "all of section 8 is implemented and green" to be corrected because
test_layer_fails_on_a_stale_library did not exist. It now reads "Every property in section 8 is
covered. Two of its rows are covered under different names, and this section is the record of
which", quotes the false sentence it replaces, and maps both rows to what was actually built.
Two rows, not one. You found row 9 by checking the other eight instead of fixing the instance I
named. That is this project's own rule — a correction is not complete until you have searched for
the same claim elsewhere — applied without being asked, and it is the half of the fix I did not
request.
The row 7 entry is also more honest than the design it replaces: it says the original mtime-versus-
pg_postmaster_start_time() check is near-vacuous on its own, because every suite initdbs
fresh so the postmaster always postdates the .so. That is the same conclusion I reached
independently on #898's equivalent arm, arrived at from the other direction.
The count reconciles, checked from disk rather than read:
§8a 74 tests in 6 files
TESTS.md 74 tests in 6 files
disk 18 + 8 + 8 + 19 + 14 + 7 = 74
And the boundary is reasoned rather than asserted: TESTS.md is under selftest/350, the design
document deliberately is not, because a design record describes decisions rather than inventory.
That is a better answer than extending the gate, which is what I would have suggested.
plan_marker, and the hole neither of us had found
I named it the one to fix first. selftest/370 holds all three arms now — three, because you
found a third that I did not:
plan_marker([], key, absent=True) -> 1 passed, exit 0
An absence claim satisfied by nothing being there at all. A plan that never arrived is
indistinguishable from a plan that legitimately lacks the node, so the arm could not fail in the
case it most needed to. That is the same shape as an empty-side comparison, one level up, and it
sat underneath both of the arms I did name.
Everything else from the first review
The guard census extended to the whole corpus and to your own additions (12 of 17), with
unreachable by subsumption named as a second cause I had missed. The build blocker closed and
driven through pgc_build_and_install so there is one implementation. The port band reading the
kernel floor, using portlib.sh's own arithmetic, and walking on collision behind a real bind test.
One thing for the merge, not this review
The hazard I posted on both PRs stands, and #898 is approved so it will likely land first:
whoever merges second must put the stamp write inside pgc_build_and_install, not leave it in
pgc_setup. Git conflicts rather than merging cleanly, so a human is forced to look — but the
comment at the conflict site argues for the wrong resolution, and this PR's
test_build_refusal.py drives that function directly, so getting it wrong means the pytest harness
builds, installs, and reports freshness UNVERIFIED while looking fine.
And the standing consequence
Under the owner's rule of 2026-09-09 — every new test written as both a .sh suite and a pytest
test in the same change — this PR is the prerequisite for every test written after it. That is a
heavier load than "the first of three, 0.18% of the suites" was scoped to carry, and it is the
reason I held on the gate rather than on the delta. It is worth carrying now.
5ec6619 to
38cb7fc
Compare
|
Rebased onto The conflict and the resolutionOne hunk, pgc_write_source_stamp \
"$(pgc_source_stamp_path "$_pgc_bi_src" "$_pgc_bi_major")" \
"$(pgc_source_fingerprint "$_pgc_bi_src")"Asserted rather than eyeballed: And the comment is reworded rather than moved, because #898's "the stamp is written HERE and nowhere else" meant not in the skip-build branch — true when written, and after the extraction it points a resolver at the wrong line. It now states the rule as a property of the path, names why the write lives in the function, and records that the old wording misleads at the conflict. Verified on the rebased treeAnd the detector that distinguishes this resolution from the wrong ones: That file is what the "leave it in Independently reachedThe other session resolved the same conflict in its own worktree and arrived at the identical answer — same location, same locals, same "reword the comment rather than move it" conclusion — and verified it with a build/edit/no-build A/B plus Two things they raised, one of which I checked and answered
Nothing else outstanding here. |
NOT PUSHED PENDING @jdatcmd's DECISION. This PR is APPROVED and this repository does not dismiss stale reviews, so pushing would make an approval cover three changes nobody reviewed. Committed locally so the work is not lost. All three are the same failure this PR exists to prevent -- the run reports FRESH while the binary is stale -- and all three were reproduced on my own box before being fixed, not inferred from the review. --- 1. THE WRITER COULD NOT REPORT FAILURE ------------------------------------ pgc_write_source_stamp() { printf '%s\n' "${2:-}" > "${1:-/dev/null}" 2>/dev/null || true } `|| true` made it always return 0, so BOTH controllers' warning branches were unreachable -- run_all_versions.sh and devloop.sh each wrap the call in `if (...)` to say so when the stamp cannot be written. Reproduced: write_rc=0 exists=no And each of those call sites carries a comment I wrote saying "NOT `|| true`. If the stamp cannot be written ... this stops being a controller with nothing saying so." The comment argued for a guarantee the function it called did not provide, which is worse than no comment because it stops the next person checking. --- 2. THE DIGEST COULD NOT SEE A REPARTITION --------------------------------- `xargs -0 cat | md5sum` hashed the concatenated stream, with no paths and no boundaries between files. Two files that both compile, with the second's bytes moved into the first: before_hash=bfce474cc159 after_hash=bfce474cc159 initial_compile=0 repartitioned_compile=1 error: redefinition of 'x' Source that CANNOT COMPILE reported "matches the binary under test". Now each file contributes its path relative to the tree and its own digest, so the partition is part of the input and one file's bytes cannot run into the next's. This is the same class as a collision I fixed on commandprompt#897's Python side today, where the digest mixed in each file's bare NAME and src/module.c and objstore/module.c were interchangeable. Two independent implementations, the same defect, found by two different people -- which is the argument for the two becoming one. --- 3. "KEYED BY MAJOR" ALIASED DISTINCT INSTALLATIONS ------------------------ `pgc_source_stamp_path DIR MAJOR` gave `.pgc_source_stamp.18`, and the comment above it already said one tree installs into several prefixes each with its own binary -- so the key discarded the distinction the comment drew. Not hypothetical on this box: pg18a pkglibdir=/usr/local/pg18a/lib/postgresql pg18n pkglibdir=/usr/local/pg18n/lib/postgresql stamp_a=/tree/.pgc_source_stamp.18 stamp_b=/tree/.pgc_source_stamp.18 SAME=YES Build into one prefix, run PGC_SKIP_BUILD=1 against another, and the fingerprint matches while the binary is stale -- and the postmaster arm passes too, because the freshly started server is newer than the other prefix's old .so. Now keyed on PKGLIBDIR rather than on the pg_config path, because that is where the .so lands: two pg_configs pointing at one prefix ARE one installation and should share a stamp. An unreadable pg_config gets a key derived from its own path rather than a shared "unknown", because aliasing every broken config onto one key is the same defect one level down. The signature is now `pgc_source_stamp_path DIR PG_CONFIG`; all three call sites had a pg_config in scope already. --- PROVED BY REMOVAL -------------------------------------------------------- unmutated 18e60be58200 302 passed writer swallows failure again 7040d7d67c04 1 failed digest reverts to concatenation 9c7e01986e67 2 failed stamp key reverts to major only bd42a44e1dca 3 failed restored 18e60be58200 byte-exact Each mutation asserted applied by md5 before the run, and each reddens exactly the arms that name it and no others. 14 new arms in test/selftest/340, driving the REAL functions. The stamp-key arms use FAKE pg_config scripts rather than this box's three PG18 installations, so the arm does not depend on which majors happen to be installed here. They include the two controls that keep the fix honest: the same pg_config twice must give one path, and two pg_configs pointing at one prefix must share a stamp. harness_selftest 288 -> 302, shellcheck exit 0. THE PYTEST TWIN IS OWED. Per jd's rule of 2026-09-09 these arms need a pytest half, and test/pytest/ exists only on commandprompt#897. commandprompt#897 is now approved, so the twin lands when this branch is rebased onto it -- which it must be anyway, for the stamp-write interlock. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
@jdatcmd's commandprompt#903 review found a defect this branch could not see: changing pgc_source_stamp_path from `DIR MAJOR` to `DIR PG_CONFIG` is a CONTRACT change, and commandprompt#897 added a caller I never swept because it did not exist when I wrote the sweep. lib.sh:203 "$(pgc_source_stamp_path "$_pgc_bi_src" "$_pgc_bi_major")" He merged the two branches and looked, which is why he found it and I did not: test/lib.sh CONFLICTS -- but in pgc_setup, not at line 203. Line 203 merges CLEANLY and is then wrong, so resolving the conflict you are shown leaves the defect behind. The hunk nobody is asked to resolve is the one that breaks. WHAT IT COSTS, reproduced here and matching his measurement to the character: correct (PG_CONFIG): .pgc_source_stamp.18.603d6145 a major passed instead: .pgc_source_stamp.0.nolib6f4 `pgc_major_of 18` finds no version in the string "18", so the major becomes 0 and the id becomes a hash of the literal "18". Writer and reader then address different files and never meet: the reader finds nothing, the verdict is `unknown`, and unknown is DELIBERATELY not a failure -- so every suite prints "freshness UNVERIFIED" and nothing says why. And `nolib6f4` derives from "18", so pg18a, pg18n and pg18_san collide again on the writer path, which is the defect fix 3 of this PR closes. FIXED: line 203 passes "$_pgc_bi_cfg". Both surviving call sites now pass a pg_config, asserted rather than eyeballed: call sites and the argument each passes: $_pgc_bi_cfg $PGC_PG_CONFIG TWO ARMS, BECAUSE HE ASKED FOR THE CLASS AND NOT THE INSTANCE. * A SWEEP over every caller in test/, reddening for any second argument that is major-shaped -- a bare integer, `$PGC_MAJOR`, or a name ending `_major`. It names the file and line. This catches the shape anywhere it appears, including in a file this PR does not touch. * AN END-TO-END AGREEMENT ARM, which is the property that actually matters. It drives the REAL pgc_build_and_install with `make` stubbed on PATH, then globs for what actually landed and compares it with what the real reader looks for, then reads the value back and asserts the verdict is `fresh`. Any disagreement about which file the stamp lives in reddens here regardless of shape -- a renamed variable, a reordered argument, a third caller nobody swept. Nothing else in this file asserted that the writer and the reader agree, which is what makes the freshness check a check. PROVED BY REMOVAL, with his exact line put back: unmutated 967aa241f8cc 366 passed line 203 -> _pgc_bi_major 9d4eb0f2cd2f 4 failed: no caller passes a major ... : got [1: test/lib.sh:203] the writer writes the file the reader looks for: got [....pgc_source_stamp.0.nolib6f4] and the reader reads back the fingerprint: got [] want [4553f83a29a3] so the verdict is fresh, not unknown: got [unknown] want [fresh] restored 967aa241f8cc byte-exact Four arms, one defect, and the silent failure -- `unknown` -- is now loud. THE SWEEP CAUGHT ITS OWN FIXTURE FIRST. Written literally, the bad-caller fixture IS a bad caller as far as a tree-wide grep is concerned, and the sweep found it at its own line on the first run. Assembled instead, the way selftest 320 assembles its forbidden line, for the reason 320 states: a test for a pattern must not contain the pattern. Rebased onto 6364e22 (commandprompt#897 merged). harness_selftest 302 -> 366, shellcheck 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
test/hilbert_locality.sh measures the one thing #889 was added for and that neither hilbert_curve.sh nor hilbert_cluster.sh can see: whether laying a table on the Hilbert curve puts two-dimensionally near rows in the same row group. One fixture, 200,000 rows over a [0,100000) square in two int columns, materialised once into a heap table and loaded into both arms from there. stripe_row_limit 1500 is deliberately non-dyadic; 134 groups on both arms. One arm gets cluster() (Z-order), the other cluster_hilbert(). The suite then sums the engine's own "Columnar Chunk Groups Read" over 60 deterministic window placements at each of four window sizes. The result is pinned as EXACT INTEGERS, not as a threshold: box z_total h_total z/h 2000 241 118 2.0424 5000 351 209 1.6794 12000 588 402 1.4627 30000 1624 1313 1.2369 A threshold is the thing someone lowers when it reddens. h < z is asserted separately at every box, so a reader can tell "the layout moved" from "Hilbert stopped winning". The controls are what make the ratio mean anything. The partition digest is order-INDEPENDENT (per group, one string from both columns' min/max; those strings sorted, then hashed), because a digest ordered by group_number reports the NUMBERING and calls two identical partitions different. Two tables on the same curve hash equal, and a dense 256x256 dyadic grid hashes equal across the two curves -- the degenerate case the design predicted. If the two partitions are not different the suite refuses to report a ratio at all: measured, that mutation gives 2 failed + 12 unrunnable and exits INCOMPLETE. test/pytest/test_hilbert_locality.py is the same properties through the pytest harness of #897, which is not merged; the file says so in its header and cannot run on main. Run against #897 assembled beside it, it reproduces all eight integers: 16 passed. Neither file is registered in test/run_all_versions.sh yet. Verified on PostgreSQL 18.4, prefix /usr/local/pg18_loc889: 61 passed + 0 failed + 0 unrunnable = 61, hilbert_locality.sh: PASSED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
…rgin (#889) The eight pinned integers were presented as the arm the suite exists for. Four mutations against them say otherwise, and one of them reddened a rule the header told the reader to follow. WHAT MOVED, MEASURED ON PG 18.4, PREFIX /usr/local/pg18_fix889 Baseline, reproduced from a clean tree twice: 65 passed + 0 failed + 0 unrunnable, digests 2169ae4551d8 and 1706e49ef5a2, pins 241:118, 351:209, 588:402, 1624:1313, z/h 2.0424, 1.6794, 1.4627, 1.2369. The pytest twin, run against #897 head 5f3dedb in a scratch worktree, produced the same eight numbers: 18 passed. A CHANGED CURVE is caught by arm 2's digest pins, not by the integers. A point reflection inside cluster_hilbert_transpose gave digest e64e00017c5a and h=117/202/403/1301; a swap of the clustering axes before the transpose gave b858f6a8a300 and h=130/208/409/1316. Both reddened the digest pin on the same run as the integers, two hundred lines upstream of them. A CHANGED READER, AT AN UNCHANGED LAYOUT, is what only the integers catch. Refusing to skip odd-numbered row groups in src/columnar_reader.c left both digests exactly at their pins and arms 1 and 3-7 green, and moved all eight integers: z=4144/4196/4317/4848, h=4083/4133/4228/4678. AND IT KEPT h < z GREEN AT EVERY BOX while z/h fell from 2.0424 to 1.0149 -- Hilbert winning by 61 groups out of 4,144, reported as PASS. The header's rule that "the pins moved but h < z still holds" means a benign layout change was therefore false. It is corrected, and a per-box margin floor is asserted beside the pins: z/h at least 1.80, 1.48, 1.28, 1.10, about 88% of the measured ratio. THE GROSS CASE, the transpose gutted so cluster_hilbert() lays Z-order, is caught by arm 2 alone: 39 passed + 2 failed + 16 unrunnable = 57, both failures arm 2's, and nm -S reported the gutted function at 5 bytes in the installed .so. THE ONE HOLE FIXED The arm "control: and that partition is the measured Z-order arm's" was the only digest comparison in the file not routed through differs(). check_text refuses an empty expectation but NO_PARTITION is not empty, so two failed reads compared equal and passed. REMOVAL PROOFS - The margin floor: under the reader mutation all four floor arms report BELOW THE FLOOR (z/h=1.0149, 1.0152, 1.0211, 1.0363) while h < z passes at every box. Under the two valid curve variants the floors stay green and only the pins red, so the two arms say different things about one run. - The differs() fix: with partition_digest() pointed at a storage_id that does not exist, the arm goes from PASS on the old text to "got [UNMEASURED[a=NO_PARTITION]] want [IDENTICAL]" on the new one. Same mutation, one arm flipped: 36 passed + 5 failed before, 35 passed + 6 failed after. - The refusal: with the transpose gutted, sixteen UNRUN lines and "39 passed + 2 failed + 16 unrunnable = 57"; with both arms loaded FROM src OFFSET 1, the same sixteen refusals and "35 passed + 6 failed + 16 unrunnable = 57". THE PYTEST TWIN Its assertions now carry the bash check names verbatim, prefixes included, and nested calls are hoisted out of the expect() arguments so compare_to_bash.py can read them: 27 names missing before, 13 after, and all 13 are accounted for in the docstring -- eleven interpolate a shell variable, two are lost to the comparator's own regex, which takes the first string literal in the call. Four properties the port did not carry are added: the exact source row count, the exact 400,000-row sum in place of two at_least floors that an arm loaded twice would satisfy, a named premise per layout verb (the port of crun), and an empty-relation sentinel shaped like QUERY_ERROR. Measured: the old oracle's 'EMPTY' passes expect.hash on two genuinely empty relations; the new one is refused as "the left side is a failed query". Two gaps are recorded rather than papered over, both #897's to close. expect.cannot_run makes a test PASS, because pytest_runtest_call reads only rec.count -- with both arms laid Z-order the twin reports "1 failed, 17 passed" where bash reports sixteen unrunnable. And plan_marker has no removal proof: replacing its present-arm raise with pass leaves 50 tests green. Deliberately still not registered in test/run_all_versions.sh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
The red-suite rule the earlier commits followed: a suite stays out of SUITES until it passes, because a red suite in the matrix is everyone's problem. It passes -- 65 checks, 0 failed, and every pinned integer and both digests reproduce on a second prefix and build dir. The pytest twin is NOT registered anywhere, and cannot be: it is blocked on #897 and its header now pins that dependency to b785795 rather than to a branch name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
Two changes from the #902 review, both from OffgridwithJD. CONTEXT.md's twin rule now says to pin the SHA the twin was tested against rather than the branch name. Their argument is the one that convinced me: a branch name is not checkable later, and it is why they could verify my claim at all. The harness branch moved three times while the first twin was being written, and two of those moves changed its content -- so "blocked on #897" and "blocked on #897 at b785795" are different claims and only one can be falsified. Same reason a tag is read from the API rather than from a local ref, which I got wrong earlier today and filed a false issue over. The twin's header records that #897 moved a fourth time, to 9064a46, and DELIBERATELY DOES NOT UPDATE THE PIN. The point of a SHA is to say what was tested. What is recorded instead is why the pin still describes the current head, verified here rather than taken from the push notice: b785795 test/pytest tree = b20ad7e 9064a46 test/pytest tree = b20ad7e whole delta = 30 lines in one test/selftest/ file the harness never reads NOT CHANGED, deliberately: the five x86_64 build failures on this PR are the PGDG apt mirror, not this branch. The mirror is serving a Release file created at 17:16:59 alongside a component index last modified at 09:41:12, so the index cannot match the manifest describing it. Two attempts twenty minutes apart produced byte-identical hashes, which rules out a race. #898 at 6939bba and #897 at b785795 both went fully green before 17:16 and both #897 at 9064a46 and this branch fail after it, with #897's delta being thirty lines in a directory no build job reads. aarch64 passed all five majors throughout. Patching ci.yml around a mirror that is mid-sync would outlive the outage and get copied. docs_style.sh: 9 checks, PASSED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
#897 merged as 6364e22, so the pytest harness and this twin are in one tree for the first time and the dependency the header described is now satisfiable. The corpus gate #897 brought with it went RED the moment the rebase put the two together, exactly as the header predicted: FAIL every test file and every test in the corpus is named in TESTS.md: got [[13: test_hilbert_locality.py test_every_layout_verb_ran_without_raising ...]] FAIL and the totals it states are the totals on disk: got [74 6] want [86 7] That is the gate working, not a problem: it names the file and every test in it rather than reporting a count that moved. So TESTS.md gains section 9 -- twelve tests, each with the wrong state it refuses -- and the totals become 86 in 7. The section says what the twin does NOT carry, because that is the part a reader would otherwise assume: the exact-integer pins are the bash suite's, and the twin asserts only that Hilbert reads fewer groups at every box. hilbert_locality.sh's header records why the integers exist at all -- for a CURVE change the digest pins upstream catch it first, so their real domain is a changed READER at an unchanged layout. The SHA pins in the twin's header are kept. They are the record of what was tested against what, and #897's branch moved four times while this file was being written -- twice with a changed tree. A pin that is deleted once the dependency lands destroys the only evidence that the claim was ever checkable. harness_selftest 342, hilbert_locality 65, docs_style 9. COPT=-Werror, 0 warnings, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
THE TWIN IS OWED AND NOW PAYABLE. selftest/340's stamp arms had no pytest half because test/pytest/ did not exist on main. commandprompt#897 merged, so it does. Four arms, driving the SHELL functions through bash rather than reimplementing them -- which is the whole lesson of this PR applied to its own tests. AND WRITING IT FOUND A FOURTH INSTANCE OF THE SAME DEFECT, on main, in the implementation @linuxhikerpm had already fixed once. source_fingerprint() in pgc_cluster.py says in its own docstring that it uses "the same input set as pgc_source_fingerprint in test/lib.sh". It did not. The shell hashes each build directory's *.c, *.h AND Makefile; the Python read only *.c and *.h there: baseline shell=45be41a5c47b python=bea88c7d79ca objstore/Makefile edited shell=cfb8f4553041 python=bea88c7d79ca Editing objstore/Makefile changes how that module is BUILT. The shell hash moves; the Python one does not; build_once then reports "already-built" and the pytest corpus measures a stale module. That is @linuxhikerpm's commandprompt#897 finding one layer over -- they found the module's SOURCES missing from this implementation, and the module's MAKEFILE was still missing after that was fixed. I found it by writing the twin and asking what the docstring's claim would look like as an assertion, not by reading the code. THE ARM ASSERTS WHAT THE DOCSTRING CLAIMED AND NOT MORE. The two hashes are NOT required to be equal: they are different digests over the same files, used independently, and requiring equality would couple two things that have no reason to be coupled. What "the same input set" means is that THE SAME EDIT MOVES BOTH, so the arm walks five edits -- a source, a module source, a module Makefile, the top-level Makefile, the control file -- and requires both hashes to move for each. Red before the fix, exactly and only where the defect was: editing a module Makefile moves both fingerprints: got 'True False' want 'True True' The other four edits already moved both, which is why this had survived two people looking at it. THE COUNT THAT MATTERS. Two implementations of one idea have now been separately wrong, separately fixed, and a third party had to find each one. I will open the "make them one implementation" issue when this lands; this commit is the fourth data point for it, not an argument against it. Verified: harness_selftest 366 passed + 0 failed + 0 unrunnable PASSED docs_style 9 checks PASSED pytest 78 passed serial, 78 passed -n 4, marker cleared for each shellcheck -S error -s bash test/*.sh test/selftest/*.sh exit 0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
…ommandprompt#432) An enumeration ran in the audit container against pytest 9.1.1, xdist 3.8.0 and psycopg 3.3.5, with every mode required to be DEMONSTRATED BY AN ACTUAL RUN rather than described. It produced 79 modes, 73 of them executed, and a refusal design for 74. WHAT DID NOT RUN, SAID FIRST. The adversarial stage that would have attacked each refusal was cut off by a session limit: 148 attacks started, 0 completed. So the summary line reading "defeated: 0" counts zero defeats out of ZERO ATTEMPTS, and none of the 74 designs has met an adversary. VACUITY_MODES.md says so in its own section rather than leaving the number to be misread. CHECKING THE LAYER AGAINST THE INVENTORY FOUND THREE GAPS. Two are here; the third landed first, in commandprompt#897, and this commit now defers to it. expect.num(-1, -1) passed. cursor.rowcount is -1 when no count is available and 1 for an unfetched SELECT, and both are numbers. expect.rowcount now refuses the sentinel and says what it is. A broad except was forbidden IN A COMMENT, which enforces nothing. After any failed statement psycopg raises for every later one, so one `except Exception` hides the real error and all its successors. It is now uncollectable. plan_marker(absent=True) returned a pass against []. That gap is closed on main by commandprompt#897's own guard, so the two tests this commit wrote for it are DROPPED rather than shipped beside it -- two tests for one property under two names is what makes a corpus hard to read, and the doc gate would then require documenting both. Independent discovery is worth recording; a duplicate test is not. AND THE BROAD-EXCEPT GUARD IMMEDIATELY REJECTED CODE ALREADY ON MAIN. Rebasing it onto 6364e22 turned the whole run red at collection: ERROR: ... test_build_refusal.py:340 except Exception catches Exception broadly -- catch the specific exception class instead. That is my own arm from commandprompt#897, catching a failed make_cluster broadly. Narrowed to (FileNotFoundError, RuntimeError, OSError) -- a missing pg_config raises FileNotFoundError out of the subprocess layer, measured, and anything else now escapes and fails loudly, which is what should happen to an error the arm did not predict. The guard earned its place before this branch was opened. Written first as a line regex, the guard fired on the forbidden shape appearing inside a pytester.makepyfile STRING and so rejected the layer's own tests. It now parses with ast, where a handler inside a string literal is not an ExceptHandler node. A line regex over source cannot tell code from a string, which is the same mistake as matching a plan by substring. One assumption of mine was refuted by checking: expect.rows does NOT sort, so ordered claims are testable through it. The collapse comes from CALLERS sorting, which test_native_projection.py does deliberately. Verified: harness_selftest 342 passed + 0 failed + 0 unrunnable PASSED docs_style 9 checks PASSED pytest 76 passed serial, 76 passed -n 4, marker cleared for each shellcheck -S error -s bash test/*.sh test/selftest/*.sh exit 0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
test/hilbert_locality.sh measures the one thing #889 was added for and that neither hilbert_curve.sh nor hilbert_cluster.sh can see: whether laying a table on the Hilbert curve puts two-dimensionally near rows in the same row group. One fixture, 200,000 rows over a [0,100000) square in two int columns, materialised once into a heap table and loaded into both arms from there. stripe_row_limit 1500 is deliberately non-dyadic; 134 groups on both arms. One arm gets cluster() (Z-order), the other cluster_hilbert(). The suite then sums the engine's own "Columnar Chunk Groups Read" over 60 deterministic window placements at each of four window sizes. The result is pinned as EXACT INTEGERS, not as a threshold: box z_total h_total z/h 2000 241 118 2.0424 5000 351 209 1.6794 12000 588 402 1.4627 30000 1624 1313 1.2369 A threshold is the thing someone lowers when it reddens. h < z is asserted separately at every box, so a reader can tell "the layout moved" from "Hilbert stopped winning". The controls are what make the ratio mean anything. The partition digest is order-INDEPENDENT (per group, one string from both columns' min/max; those strings sorted, then hashed), because a digest ordered by group_number reports the NUMBERING and calls two identical partitions different. Two tables on the same curve hash equal, and a dense 256x256 dyadic grid hashes equal across the two curves -- the degenerate case the design predicted. If the two partitions are not different the suite refuses to report a ratio at all: measured, that mutation gives 2 failed + 12 unrunnable and exits INCOMPLETE. test/pytest/test_hilbert_locality.py is the same properties through the pytest harness of #897, which is not merged; the file says so in its header and cannot run on main. Run against #897 assembled beside it, it reproduces all eight integers: 16 passed. Neither file is registered in test/run_all_versions.sh yet. Verified on PostgreSQL 18.4, prefix /usr/local/pg18_loc889: 61 passed + 0 failed + 0 unrunnable = 61, hilbert_locality.sh: PASSED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
…rgin (#889) The eight pinned integers were presented as the arm the suite exists for. Four mutations against them say otherwise, and one of them reddened a rule the header told the reader to follow. WHAT MOVED, MEASURED ON PG 18.4, PREFIX /usr/local/pg18_fix889 Baseline, reproduced from a clean tree twice: 65 passed + 0 failed + 0 unrunnable, digests 2169ae4551d8 and 1706e49ef5a2, pins 241:118, 351:209, 588:402, 1624:1313, z/h 2.0424, 1.6794, 1.4627, 1.2369. The pytest twin, run against #897 head 5f3dedb in a scratch worktree, produced the same eight numbers: 18 passed. A CHANGED CURVE is caught by arm 2's digest pins, not by the integers. A point reflection inside cluster_hilbert_transpose gave digest e64e00017c5a and h=117/202/403/1301; a swap of the clustering axes before the transpose gave b858f6a8a300 and h=130/208/409/1316. Both reddened the digest pin on the same run as the integers, two hundred lines upstream of them. A CHANGED READER, AT AN UNCHANGED LAYOUT, is what only the integers catch. Refusing to skip odd-numbered row groups in src/columnar_reader.c left both digests exactly at their pins and arms 1 and 3-7 green, and moved all eight integers: z=4144/4196/4317/4848, h=4083/4133/4228/4678. AND IT KEPT h < z GREEN AT EVERY BOX while z/h fell from 2.0424 to 1.0149 -- Hilbert winning by 61 groups out of 4,144, reported as PASS. The header's rule that "the pins moved but h < z still holds" means a benign layout change was therefore false. It is corrected, and a per-box margin floor is asserted beside the pins: z/h at least 1.80, 1.48, 1.28, 1.10, about 88% of the measured ratio. THE GROSS CASE, the transpose gutted so cluster_hilbert() lays Z-order, is caught by arm 2 alone: 39 passed + 2 failed + 16 unrunnable = 57, both failures arm 2's, and nm -S reported the gutted function at 5 bytes in the installed .so. THE ONE HOLE FIXED The arm "control: and that partition is the measured Z-order arm's" was the only digest comparison in the file not routed through differs(). check_text refuses an empty expectation but NO_PARTITION is not empty, so two failed reads compared equal and passed. REMOVAL PROOFS - The margin floor: under the reader mutation all four floor arms report BELOW THE FLOOR (z/h=1.0149, 1.0152, 1.0211, 1.0363) while h < z passes at every box. Under the two valid curve variants the floors stay green and only the pins red, so the two arms say different things about one run. - The differs() fix: with partition_digest() pointed at a storage_id that does not exist, the arm goes from PASS on the old text to "got [UNMEASURED[a=NO_PARTITION]] want [IDENTICAL]" on the new one. Same mutation, one arm flipped: 36 passed + 5 failed before, 35 passed + 6 failed after. - The refusal: with the transpose gutted, sixteen UNRUN lines and "39 passed + 2 failed + 16 unrunnable = 57"; with both arms loaded FROM src OFFSET 1, the same sixteen refusals and "35 passed + 6 failed + 16 unrunnable = 57". THE PYTEST TWIN Its assertions now carry the bash check names verbatim, prefixes included, and nested calls are hoisted out of the expect() arguments so compare_to_bash.py can read them: 27 names missing before, 13 after, and all 13 are accounted for in the docstring -- eleven interpolate a shell variable, two are lost to the comparator's own regex, which takes the first string literal in the call. Four properties the port did not carry are added: the exact source row count, the exact 400,000-row sum in place of two at_least floors that an arm loaded twice would satisfy, a named premise per layout verb (the port of crun), and an empty-relation sentinel shaped like QUERY_ERROR. Measured: the old oracle's 'EMPTY' passes expect.hash on two genuinely empty relations; the new one is refused as "the left side is a failed query". Two gaps are recorded rather than papered over, both #897's to close. expect.cannot_run makes a test PASS, because pytest_runtest_call reads only rec.count -- with both arms laid Z-order the twin reports "1 failed, 17 passed" where bash reports sixteen unrunnable. And plan_marker has no removal proof: replacing its present-arm raise with pass leaves 50 tests green. Deliberately still not registered in test/run_all_versions.sh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
The red-suite rule the earlier commits followed: a suite stays out of SUITES until it passes, because a red suite in the matrix is everyone's problem. It passes -- 65 checks, 0 failed, and every pinned integer and both digests reproduce on a second prefix and build dir. The pytest twin is NOT registered anywhere, and cannot be: it is blocked on #897 and its header now pins that dependency to b785795 rather than to a branch name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
Two changes from the #902 review, both from OffgridwithJD. CONTEXT.md's twin rule now says to pin the SHA the twin was tested against rather than the branch name. Their argument is the one that convinced me: a branch name is not checkable later, and it is why they could verify my claim at all. The harness branch moved three times while the first twin was being written, and two of those moves changed its content -- so "blocked on #897" and "blocked on #897 at b785795" are different claims and only one can be falsified. Same reason a tag is read from the API rather than from a local ref, which I got wrong earlier today and filed a false issue over. The twin's header records that #897 moved a fourth time, to 9064a46, and DELIBERATELY DOES NOT UPDATE THE PIN. The point of a SHA is to say what was tested. What is recorded instead is why the pin still describes the current head, verified here rather than taken from the push notice: b785795 test/pytest tree = b20ad7e 9064a46 test/pytest tree = b20ad7e whole delta = 30 lines in one test/selftest/ file the harness never reads NOT CHANGED, deliberately: the five x86_64 build failures on this PR are the PGDG apt mirror, not this branch. The mirror is serving a Release file created at 17:16:59 alongside a component index last modified at 09:41:12, so the index cannot match the manifest describing it. Two attempts twenty minutes apart produced byte-identical hashes, which rules out a race. #898 at 6939bba and #897 at b785795 both went fully green before 17:16 and both #897 at 9064a46 and this branch fail after it, with #897's delta being thirty lines in a directory no build job reads. aarch64 passed all five majors throughout. Patching ci.yml around a mirror that is mid-sync would outlive the outage and get copied. docs_style.sh: 9 checks, PASSED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
the first time and the dependency the header described is now satisfiable. The corpus gate #897 brought with it went RED the moment the rebase put the two together, exactly as the header predicted: FAIL every test file and every test in the corpus is named in TESTS.md: got [[13: test_hilbert_locality.py test_every_layout_verb_ran_without_raising ...]] FAIL and the totals it states are the totals on disk: got [74 6] want [86 7] That is the gate working, not a problem: it names the file and every test in it rather than reporting a count that moved. So TESTS.md gains section 9 -- twelve tests, each with the wrong state it refuses -- and the totals become 86 in 7. The section says what the twin does NOT carry, because that is the part a reader would otherwise assume: the exact-integer pins are the bash suite's, and the twin asserts only that Hilbert reads fewer groups at every box. hilbert_locality.sh's header records why the integers exist at all -- for a CURVE change the digest pins upstream catch it first, so their real domain is a changed READER at an unchanged layout. The SHA pins in the twin's header are kept. They are the record of what was tested against what, and #897's branch moved four times while this file was being written -- twice with a changed tree. A pin that is deleted once the dependency lands destroys the only evidence that the claim was ever checkable. harness_selftest 342, hilbert_locality 65, docs_style 9. COPT=-Werror, 0 warnings, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
The rebase onto `bfdd1f9` collided on TESTS.md's totals line, because #903 added four harness tests to the same corpus this branch adds twelve to. Both sides of the conflict were wrong for the composed tree, so the number was COUNTED with the gate's own function rather than picked from either side: python3 -c "... corpus_tests(Path('test/pytest')) ..." files=7 tests=90 test_build_refusal.py 22 test_connection.py 8 test_docs_cover_the_corpus.py 8 test_guards_pinned.py 19 test_hilbert_locality.py 12 test_layer.py 14 test_native_projection.py 7 Then I passed 90 to `--pgc-expect-tests` and the run refused: ERROR: collected 96 test(s) but expected 90. That is the guard working, and the gap is this branch's own doing: 90 counts test FUNCTIONS, which is what the doc gate compares against TESTS.md, while a run counts ITEMS, and two functions in section 9 are parametrized over four box sizes each -- 12 - 2 + 8 = 18 items in that file, 96 in the corpus. Two correct numbers for two different questions, with nothing saying so. The header now says which is which and which one `--pgc-expect-tests` wants. The twin's `got [74 6] want [86 7]` is NOT regenerated. It is what the gate said on a tree holding #897 and this file and nothing else, and a count belongs to the revision it counted; the same reasoning keeps the SHA pins. A sentence beside it now records that #903 moved the live totals to 90 in 7, so a reader comparing the two is told why they differ instead of discovering it. Verified on the rebased head, my own prefix /usr/local/pg17_904: harness_selftest.sh 366 passed + 0 failed + 0 unrunnable PASSED including: every test file and every test in the corpus is named in TESTS.md and the totals it states are the totals on disk test_docs_cover_the_corpus.py 8 passed hilbert_locality.sh 65 passed + 0 failed + 0 unrunnable PASSED docs_style.sh PASSED Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
…ed (#889) OffgridwithJD's fourth review point, and they had to make it twice. I reported this paragraph gone and it was not: my check was grep -n "DECLARING A TEST UNRUNNABLE MAKES IT PASS" test/pytest/... and the file wraps that phrase across lines 66 and 67, so the grep found nothing and I read nothing as evidence of absence. I reported "ABSENT at HEAD (good)" to a peer who then checked the artifact and found it byte-identical at two heads. A single-line grep for a phrase that is line-wrapped cannot report what it was asked; the same class as every instrument this branch has been cataloguing, and this one was mine. The re-check is a regex tolerant of the break, and it is what now says all four stale claims are gone. The paragraph itself was worse than merely stale: line 29 said "#897 HAS SINCE MERGED, so this file is no longer blocked" and line 77 told the reader the change it needed from #897 had not happened. The contradiction had moved inside one file rather than being resolved. Re-measured here rather than taken from the merge or from the peer: UNRUN test_p.py::test_cannot: ABSENT_FIXTURE: no corpus checks unrunnable: 1 exit code = 67 So the twin DOES carry the bash suite's refusal. One precision the replacement adds beyond what was suggested, because I hit it while measuring: pytest's own per-item tally still prints "1 passed" for the declaring test. The session exit and the unrunnable count are what carry the refusal, not the tally -- and a reader who greps for "passed" reaches the wrong conclusion, which is the same mistake in the other direction. The measurement against 5f3dedb is KEPT, reframed as history. It is the record of what the gap was and it should outlive the gap; deleting it would leave the claim "this was once broken" resting on nothing. The section heading moved with it, since "ONE THING THIS FILE CANNOT DO" was false as of #897. Verified on this head, prefix /usr/local/pg17_904: harness_selftest.sh 366 passed + 0 failed + 0 unrunnable PASSED hilbert_locality.sh 65 passed + 0 failed + 0 unrunnable PASSED pytest corpus 96 passed (90 functions in 7 files) docs_style.sh PASSED Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK
Answers the review on commandprompt#905. Four defects, three of them in the layer's own guards and one in the document that describes them. expect.refusal matched the whole traceback, not the error --------------------------------------------------------- `expect.refusal` built its patterns as `*{p}*` and ran them over the pytest output. pytest prints the ENCLOSING FUNCTION'S SOURCE in a traceback, so a pattern naming the thing the guard refuses matched the fixture's own source line and passed whether or not the guard fired. Anchored to `E*{p}*`, which is the error line pytest actually emits. This is on main from commandprompt#897 and it is the largest of the four: 13 merged arms were asserting nothing. Verified by neutering each guard in turn -- with the guard live the arm passes, with it removed the arm now fails. Four arms tested, all four held. plan_marker carried a dead branch --------------------------------- `nodes == 0` could not be reached: `if not nodes:` above it returns first. Removed. `nodes == 0` now occurs zero times and `if not nodes:` once. the broad-except scan missed every tuple handler ------------------------------------------------- `except (ValueError, Exception):` is as broad as `except Exception:` and the scan walked past it, because it inspected the handler type only when that type was a bare Name. It now inspects each member of a Tuple. All five spellings verified: `Exception`, `(ValueError, Exception)`, `BaseException` and the bare `except:` are refused; `except ValueError:` still passes as the control. the inventory could not be checked, so it drifted -------------------------------------------------- README.md said 23 refused modes and VACUITY_MODES.md said 27, and a reader could check NEITHER, because the document offered no rule for what counts as a mode. That is the defect this directory exists to refuse, committed by the document describing the refusal. Section 1a now states the rule -- a mode is a backticked kebab-case identifier of three or more words -- and reconciles the totals against it: 21 refused, 51 not, 72 named, against 79 the enumeration produced. The seven never written down are named as a gap rather than counted as coverage. The numbers are now gated in both harnesses, because a total nobody recomputes goes stale the same way twice: * `test/pytest/test_docs_cover_the_corpus.py` -- four arms over the table, the README, the gap arithmetic, and the prose totals outside the table. * `test/selftest/350-the-pytest-corpus-must-be.sh` -- the same rules, and this is the copy with teeth: nothing in the gate runs pytest. The two implementations disagreed, and the disagreement was the point. The bash reader took the first number on the line and returned 2 and 3 for totals of 21 and 51 -- the digits inside "named in section 2". Its fixture could not see it because there the label digit and the value were both 2, so there is now an arm whose only job is to tell those two readings apart. Proved able to fail, each mutation asserted applied by md5 and restored byte-exact: 1a states 22 refused, disk has 21 arm reddens a refused mode id loses its backticks arm reddens README drifts back to 23 arm reddens the gap row closes on its own arm reddens section 2's opening drifts back to 23 arm reddens the closing paragraph drifts back to 23 arm reddens TESTS.md drifts back to 23 arm reddens harness_selftest 387 passed + 0 failed + 0 unrunnable, rc=0 pytest corpus 84 passed serial and under -n 4 docs_style 9 checks PASSED shellcheck -S error clean Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
…ommandprompt#432) An enumeration ran in the audit container against pytest 9.1.1, xdist 3.8.0 and psycopg 3.3.5, with every mode required to be DEMONSTRATED BY AN ACTUAL RUN rather than described. It produced 79 modes, 73 of them executed, and a refusal design for 74. WHAT DID NOT RUN, SAID FIRST. The adversarial stage that would have attacked each refusal was cut off by a session limit: 148 attacks started, 0 completed. So the summary line reading "defeated: 0" counts zero defeats out of ZERO ATTEMPTS, and none of the 74 designs has met an adversary. VACUITY_MODES.md says so in its own section rather than leaving the number to be misread. CHECKING THE LAYER AGAINST THE INVENTORY FOUND THREE GAPS. Two are here; the third landed first, in commandprompt#897, and this commit now defers to it. expect.num(-1, -1) passed. cursor.rowcount is -1 when no count is available and 1 for an unfetched SELECT, and both are numbers. expect.rowcount now refuses the sentinel and says what it is. A broad except was forbidden IN A COMMENT, which enforces nothing. After any failed statement psycopg raises for every later one, so one `except Exception` hides the real error and all its successors. It is now uncollectable. plan_marker(absent=True) returned a pass against []. That gap is closed on main by commandprompt#897's own guard, so the two tests this commit wrote for it are DROPPED rather than shipped beside it -- two tests for one property under two names is what makes a corpus hard to read, and the doc gate would then require documenting both. Independent discovery is worth recording; a duplicate test is not. AND THE BROAD-EXCEPT GUARD IMMEDIATELY REJECTED CODE ALREADY ON MAIN. Rebasing it onto 6364e22 turned the whole run red at collection: ERROR: ... test_build_refusal.py:340 except Exception catches Exception broadly -- catch the specific exception class instead. That is my own arm from commandprompt#897, catching a failed make_cluster broadly. Narrowed to (FileNotFoundError, RuntimeError, OSError) -- a missing pg_config raises FileNotFoundError out of the subprocess layer, measured, and anything else now escapes and fails loudly, which is what should happen to an error the arm did not predict. The guard earned its place before this branch was opened. Written first as a line regex, the guard fired on the forbidden shape appearing inside a pytester.makepyfile STRING and so rejected the layer's own tests. It now parses with ast, where a handler inside a string literal is not an ExceptHandler node. A line regex over source cannot tell code from a string, which is the same mistake as matching a plan by substring. One assumption of mine was refuted by checking: expect.rows does NOT sort, so ordered claims are testable through it. The collapse comes from CALLERS sorting, which test_native_projection.py does deliberately. Verified: harness_selftest 342 passed + 0 failed + 0 unrunnable PASSED docs_style 9 checks PASSED pytest 76 passed serial, 76 passed -n 4, marker cleared for each shellcheck -S error -s bash test/*.sh test/selftest/*.sh exit 0 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
Answers the review on commandprompt#905. Four defects, three of them in the layer's own guards and one in the document that describes them. expect.refusal matched the whole traceback, not the error --------------------------------------------------------- `expect.refusal` built its patterns as `*{p}*` and ran them over the pytest output. pytest prints the ENCLOSING FUNCTION'S SOURCE in a traceback, so a pattern naming the thing the guard refuses matched the fixture's own source line and passed whether or not the guard fired. Anchored to `E*{p}*`, which is the error line pytest actually emits. This is on main from commandprompt#897 and it is the largest of the four: 13 merged arms were asserting nothing. Verified by neutering each guard in turn -- with the guard live the arm passes, with it removed the arm now fails. Four arms tested, all four held. plan_marker carried a dead branch --------------------------------- `nodes == 0` could not be reached: `if not nodes:` above it returns first. Removed. `nodes == 0` now occurs zero times and `if not nodes:` once. the broad-except scan missed every tuple handler ------------------------------------------------- `except (ValueError, Exception):` is as broad as `except Exception:` and the scan walked past it, because it inspected the handler type only when that type was a bare Name. It now inspects each member of a Tuple. All five spellings verified: `Exception`, `(ValueError, Exception)`, `BaseException` and the bare `except:` are refused; `except ValueError:` still passes as the control. the inventory could not be checked, so it drifted -------------------------------------------------- README.md said 23 refused modes and VACUITY_MODES.md said 27, and a reader could check NEITHER, because the document offered no rule for what counts as a mode. That is the defect this directory exists to refuse, committed by the document describing the refusal. Section 1a now states the rule -- a mode is a backticked kebab-case identifier of three or more words -- and reconciles the totals against it: 21 refused, 51 not, 72 named, against 79 the enumeration produced. The seven never written down are named as a gap rather than counted as coverage. The numbers are now gated in both harnesses, because a total nobody recomputes goes stale the same way twice: * `test/pytest/test_docs_cover_the_corpus.py` -- four arms over the table, the README, the gap arithmetic, and the prose totals outside the table. * `test/selftest/350-the-pytest-corpus-must-be.sh` -- the same rules, and this is the copy with teeth: nothing in the gate runs pytest. The two implementations disagreed, and the disagreement was the point. The bash reader took the first number on the line and returned 2 and 3 for totals of 21 and 51 -- the digits inside "named in section 2". Its fixture could not see it because there the label digit and the value were both 2, so there is now an arm whose only job is to tell those two readings apart. Proved able to fail, each mutation asserted applied by md5 and restored byte-exact: 1a states 22 refused, disk has 21 arm reddens a refused mode id loses its backticks arm reddens README drifts back to 23 arm reddens the gap row closes on its own arm reddens section 2's opening drifts back to 23 arm reddens the closing paragraph drifts back to 23 arm reddens TESTS.md drifts back to 23 arm reddens harness_selftest 387 passed + 0 failed + 0 unrunnable, rc=0 pytest corpus 84 passed serial and under -n 4 docs_style 9 checks PASSED shellcheck -S error clean Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
…mmandprompt#907) test/lib.sh and test/pytest/pgc_cluster.py each carried their own answer to "what was this binary built from". On 2026-09-09 the pair produced four defects between them -- two in each copy, and NOT ONE was found by whoever wrote that copy: objstore/*.c never walked python @linuxhikerpm, commandprompt#897 the bare NAME instead of the path python found while fixing the above `xargs -0 cat | md5sum`, no bounds shell @linuxhikerpm, commandprompt#898 each build dir's Makefile omitted python found while writing the twin The Python docstring asserted "the same input set as pgc_source_fingerprint in test/lib.sh" throughout all four. It was false when written and stayed false through two rounds of fixing. A prose claim of agreement is not a mechanism, and it is worse than silence because it is what stops the next person checking. Python, not shell, which is the opposite of what commandprompt#907 first proposed -------------------------------------------------------------------- jd's constraint decided it: the single implementation belongs in the more portable language. bash is largely a GNU thing; Python is present on FreeBSD and Windows where bash is not. lib.sh already requires bash, so calling a more portable interpreter from it cannot cost portability. My argument against this direction was that lib.sh invokes python3 zero times, so this escalates from "53 suites need it" to "every suite needs it at gate time". That is true and it is not a cost, for the reason above. Measured, expecting to report a subprocess penalty: shell, forking md5sum once per file 239 ms/call the module, one interpreter start 26 ms/call across 261 suites x 2 fingerprints 124 s -> 13 s The portable direction is also 9x faster. I had it backwards in both dimensions. A fifth defect, which unifying them found ------------------------------------------ `sort -z` orders by LOCALE COLLATION, and nothing in this harness pins a locale. The same tree fingerprinted two ways depending on whose machine it was: LC_ALL=C 6d122a7158d5 LC_ALL=en_US.UTF-8 0b59bd75fa4f en_US.UTF-8 is a common desktop default, so this is a developer stamping a tree and CI reading it back and calling the binary stale -- a false FATAL arriving from the environment rather than from the source. The module sorts BYTES, which is what LC_ALL=C produced and what every stamp already on disk was written with, so no existing stamp is invalidated. Arms in both harnesses. Equivalence, established rather than asserted ---------------------------------------------- A differential run of the module against the shell it replaces, over trees built to break the ways this pair has actually broken. 17 shapes, manifest AND fingerprint compared: the real source tree, minimal, a recursed module, a dir with sources but no Makefile, collation-sensitive names, a symlinked source file, a symlinked build directory, no src/, an empty tree, root .control and .sql, non-source files, spaces and punctuation, unicode, a Makefile at depth 3, a trailing slash, a /./ segment, five recursed modules AGREE=17 DIVERGE=0 Two of those are subtle enough to be worth naming. `find -type f` tests the LINK, so a symlinked source is not in the shell's manifest, while `pathlib.is_file()` FOLLOWS it and would have added one; the module excludes symlinks explicitly. And `find` does not descend a symlinked directory, so build dirs discovered through one differ -- which is why the module canonicalises the root first. The mechanism of two arms had to change with the implementation ---------------------------------------------------------------- The failed-digest arms in 340 and test_build_refusal.py stubbed `md5sum` on PATH. The digest is hashlib now, which no PATH can reach, so the stub would have left both arms GREEN while testing nothing -- the exact shape this corpus refuses. A real read failure needs a real reader who is denied, and root is denied nothing: chmod 000 is invisible to it. Measured before the arms were rewritten: as root 28a7149e07ae <- reads the mode-000 file regardless as postgres (empty) <- the failure the arm needs So the tree is built outside any mode-0700 directory and read by a second user, with a premise asserting that reader agrees with a privileged one WHILE nothing is denied -- otherwise the arm measures the user switch rather than the failure. Where no non-root user exists it records expect.cannot_run rather than passing. And the arm that would catch this issue recurring -------------------------------------------------- selftest 380's static guards follow the fingerprint to its new file, plus three new arms: neither caller may keep a private implementation, and the module may import nothing from test/pytest/. A static assertion of ABSENCE is the shape that most often cannot fail, so each was proved against the REAL files rather than only against fixtures -- a fixture proves the pattern matches something, not that the arm aimed at the real file would fire: pgc_cluster.py grows a private digest HELD lib.sh grows a private md5sum loop HELD the module imports from the pytest tree HELD harness_selftest 407 passed + 0 failed + 0 unrunnable, rc=0 pytest corpus 91 passed docs_style 9 checks PASSED shellcheck -S error clean Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
…mmandprompt#907) test/lib.sh and test/pytest/pgc_cluster.py each carried their own answer to "what was this binary built from". On 2026-09-09 the pair produced four defects between them -- two in each copy, and NOT ONE was found by whoever wrote that copy: objstore/*.c never walked python @linuxhikerpm, commandprompt#897 the bare NAME instead of the path python found while fixing the above `xargs -0 cat | md5sum`, no bounds shell @linuxhikerpm, commandprompt#898 each build dir's Makefile omitted python found while writing the twin The Python docstring asserted "the same input set as pgc_source_fingerprint in test/lib.sh" throughout all four. It was false when written and stayed false through two rounds of fixing. A prose claim of agreement is not a mechanism, and it is worse than silence because it is what stops the next person checking. Python, not shell, which is the opposite of what commandprompt#907 first proposed -------------------------------------------------------------------- jd's constraint decided it: the single implementation belongs in the more portable language. bash is largely a GNU thing; Python is present on FreeBSD and Windows where bash is not. lib.sh already requires bash, so calling a more portable interpreter from it cannot cost portability. My argument against this direction was that lib.sh invokes python3 zero times, so this escalates from "53 suites need it" to "every suite needs it at gate time". That is true and it is not a cost, for the reason above. Measured, expecting to report a subprocess penalty: shell, forking md5sum once per file 239 ms/call the module, one interpreter start 26 ms/call across 261 suites x 2 fingerprints 124 s -> 13 s The portable direction is also 9x faster. I had it backwards in both dimensions. A fifth defect, which unifying them found ------------------------------------------ `sort -z` orders by LOCALE COLLATION, and nothing in this harness pins a locale. The same tree fingerprinted two ways depending on whose machine it was: LC_ALL=C 6d122a7158d5 LC_ALL=en_US.UTF-8 0b59bd75fa4f en_US.UTF-8 is a common desktop default, so this is a developer stamping a tree and CI reading it back and calling the binary stale -- a false FATAL arriving from the environment rather than from the source. The module sorts BYTES, which is what LC_ALL=C produced and what every stamp already on disk was written with, so no existing stamp is invalidated. Arms in both harnesses. Equivalence, established rather than asserted ---------------------------------------------- A differential run of the module against the shell it replaces, over trees built to break the ways this pair has actually broken. 17 shapes, manifest AND fingerprint compared: the real source tree, minimal, a recursed module, a dir with sources but no Makefile, collation-sensitive names, a symlinked source file, a symlinked build directory, no src/, an empty tree, root .control and .sql, non-source files, spaces and punctuation, unicode, a Makefile at depth 3, a trailing slash, a /./ segment, five recursed modules AGREE=17 DIVERGE=0 Two of those are subtle enough to be worth naming. `find -type f` tests the LINK, so a symlinked source is not in the shell's manifest, while `pathlib.is_file()` FOLLOWS it and would have added one; the module excludes symlinks explicitly. And `find` does not descend a symlinked directory, so build dirs discovered through one differ -- which is why the module canonicalises the root first. The mechanism of two arms had to change with the implementation ---------------------------------------------------------------- The failed-digest arms in 340 and test_build_refusal.py stubbed `md5sum` on PATH. The digest is hashlib now, which no PATH can reach, so the stub would have left both arms GREEN while testing nothing -- the exact shape this corpus refuses. A real read failure needs a real reader who is denied, and root is denied nothing: chmod 000 is invisible to it. Measured before the arms were rewritten: as root 28a7149e07ae <- reads the mode-000 file regardless as postgres (empty) <- the failure the arm needs So the tree is built outside any mode-0700 directory and read by a second user, with a premise asserting that reader agrees with a privileged one WHILE nothing is denied -- otherwise the arm measures the user switch rather than the failure. Where no non-root user exists it records expect.cannot_run rather than passing. And the arm that would catch this issue recurring -------------------------------------------------- selftest 380's static guards follow the fingerprint to its new file, plus three new arms: neither caller may keep a private implementation, and the module may import nothing from test/pytest/. A static assertion of ABSENCE is the shape that most often cannot fail, so each was proved against the REAL files rather than only against fixtures -- a fixture proves the pattern matches something, not that the arm aimed at the real file would fire: pgc_cluster.py grows a private digest HELD lib.sh grows a private md5sum loop HELD the module imports from the pytest tree HELD harness_selftest 407 passed + 0 failed + 0 unrunnable, rc=0 pytest corpus 91 passed docs_style 9 checks PASSED shellcheck -S error clean Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
First of three for #432. This is the foundation: a cluster fixture, a direct
psycopgconnection, the vacuity-refusal layer that every later test depends on, and 25 tests.The two follow-ups are ready and held back deliberately, because each is only reviewable once this one is agreed:
Why a second harness at all, stated honestly
The bash suites are not being replaced and this does not try to.
test/carries 4,429 anchored assertions across 256 suites; this PR ports one of them. That is 0.18%. Nobody should read this as coverage.What it buys is the thing bash cannot do cheaply: a typed result.
psql -Atreturns text, so a bash oracle compares strings and anint41and atext'1'are the same value to it.psycopgreturnsint,Decimal,float,bytes,list,None, and a test can assert the type as well as the value. Per #432 this uses a direct connection everywhere and shells out topsqlonly where there is no alternative — currently nowhere in these 25 tests.The layer is the point, not the tests
A pytest suite fails open. A test that asserts nothing passes; a filter that selects nothing exits 0; a fixture that skips greens every test under it. This project has already shipped a vacuity defect, so the harness refuses those shapes rather than documenting them.
pgc_vacuity.pyis loaded for every run viapytest.ini, and it refuses:cursor.rowcountof-1, which is a number and truthyexcept, found by walking the AST rather than by line regexskip, andxfail_strict = trueso an xpass is not silently green--pgc-expect-tests N, which asserts the run's own shape, and refusesN = 0Every one of those has a red test in
test_layer.pythat runs pytest inside pytest through thepytesterfixture and asserts on the inner run's outcome. That is what proves a guard refuses rather than assuming it. Each row of the table inTESTS.mdalso records the bare-pytest behaviour it exists to stop, measured: every one of those measurements exited 0.Four of the ten layer tests are positive controls, deliberately. A guard with a bad false-positive rate gets switched off, and then whatever it replaced is gone too.
The escape hatches are all more expensive to type than the honest form:
allow_emptytakes a reason, notTrue;--pgc-expect-teststakes the real number;cannot_runtakes a reason from a closed list. None can become the default by being shorter.The port is proved against the bash suite it came from
test_native_projection.pyportstest/native_projection.sh.compare_to_bash.pyruns both and compares the property names each asserts, not the counts — agrep -c " PASSED"reported 6 of 7 because the first test's outcome shares a line with a fixture's print, and a count that is wrong for that reason looks exactly like a count that is right.Not in the gate, and why
test/run_all_versions.shdoes not run this. Registering it would add apsycopgbuild dependency to every CI leg for 0.18% of the assertions, and the point of these 25 tests today is the layer, not the coverage.README.mdsays what registering would cost and what has to be true before it is worth it.Verification
At
c8b2a9e, rebased ontoedd729e:Exit codes read without a pipe, because a pipeline reports the exit status of its last stage and that has already produced one wrong verdict in this project.
I also derived which existing suites this branch can affect rather than guessing. Nothing in
test/namestest/pytest/or the design document, so the reachable surface is the harness's own accounting and the docs checks:harness_selftestis the one that matters here:compare_to_bash.pyis recorded100755, and it failed that suite on both majors as100644with a shebang.The rebase onto
edd729ewas verified not to have touched anything in this PR —git diff c02cfc4 HEAD -- test/pytest designis empty — and the 25 were re-run at the rebased tip rather than inherited from the pre-rebase run.Reviewing this
The highest-value thing to attack is
test_layer.py. If any guard there can be made to pass with the guard removed, the layer is decoration. I have run each one that way; a second pair of hands is worth more than my own repetition.